iT邦幫忙

2022 iThome 鐵人賽

DAY 18
3
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 18

Day 18: v-slot 到底用在哪?從應用學 v-slot 語法

  • 分享至 

  • xImage
  •  

前言

要在父子元件之間傳遞變數或內容的時候,我們可以使用 props,但如果想要傳遞進去的是一段 template 片段,就要使用到 slot。

最簡單明瞭的 slot 範例:

如上述範例, Vue 會在子元件的 slot 插入 textNode (#text),渲染出 <button>Click Me<button>

我剛開始看到的時候,其實蠻疑惑的,這樣跟透過 props 傳進去,似乎沒有太大的差異,所以 slot 到底要用在哪?

開始做案子之後,才發現 v-slot 的好用之處,可以讓元件變得更彈性,透過傳入 template 來客製部份元件,所以實際上用到 v-slot 的狀況,通常不會只傳入簡單的文字內容。


Outline

接下來會用兩個範例,來示範 v-slot 在 real world 開發中好用的地方。

  1. 自訂表格元件
  2. Renderless component - 以 Reusable Transitions 為例

過程中會講解到的 slot 相關語法:

  • Default Slot
  • Named Slots
  • Slot 的 scope
    • 如何取得元件層資料

1. 表格元件

要示範 v-slot 好用之處,我覺得最棒的案例就是表格!(展示型的網站比較少用到表格,但在開發後台系統的時候很常用到。)

大部分使用表格的狀況,只需要渲染一般資料內容,像這樣:

姓名 主題 備註
1 Angela 真的好想離開 Vue 3 新手村 Composition API
2 阿傑 咩色用得好,歸剛沒煩惱 從 ECMAScript 偷窺 JavaScript Array method
3 Jade 前端蛇行撞牆記 -
4 Vic JavaScript 之路,往前邁進吧! 未來會成為JS大師的人。

只要單純呈現資料的話,Table 元件可以很簡單,利用 v-for 渲染傳進來的標頭(Array)和資料(Array[Object])就可以了,元件模板大概會像這樣:

<template>
  <table>
    <thead>
      <th></th>
      <th v-for="tableHead in props.tableHeads" :key="tableHead">
        {{ tableHead }}
      </th>
    </thead>
    <tbody>
      <tr v-for="(item, itemIndex) in props.tableBody" :key="user.name">
        <th>{{ itemIndex + 1 }}</th>
        <td v-for="(value, key, index) in item" :key="value">
          {{ value }}
        </td>
      </tr>
    </tbody>
  </table>
</template>

BUT!

有時候表格會需要一些特殊欄位,例如 <a> 連結、按鈕(點擊時呼叫 method)、圖片或 icon 等等。
不同頁面的特殊欄位又會不一樣,想要做到每次引用元件時,都可以自由增加自訂欄位格式,就可以透過 v-slot 做到。

以剛剛的範例進行擴充,加入「查看」的連結,能點擊對應的鐵人發文頁面,預期呈現的畫面如下:

姓名 主題 備註 查看
1 Angela 真的好想離開 Vue 3 新手村 Composition API 查看
2 阿傑 咩色用得好,歸剛沒煩惱 從 ECMAScript 偷窺 JavaScript Array method 查看
3 Jade 前端蛇行撞牆記 - 查看
4 Vic JavaScript 之路,往前邁進吧! 未來會成為JS大師的人。 查看

資料形式如下:

const tableHeads = ref(["姓名", "主題", "備註", "查看"]);

const tableBody = ref([
  {
    name: "Angela",
    topic: "真的好想離開 Vue 3 新手村",
    note: "Composition API",
    ithelpLink: "https://ithelp.ithome.com.tw/users/20152606/ironman/5782",
  },
//後略
])

預期使用方式:
在使用元件時,傳入 nameithelpLink 的 slot,Vue 會將這個 slot 內容塞到表格中 ithelpLink/查看那一行的格子內,引用時看起來會像下面這樣:

<template>
  <MyTable :tableHeads="tableHeads" :tableBody="tableBody">
    <!-- 查看連結 -->
    <template #ithelpLink>
      ~~客製化欄位~~
    </template>
    <template #其他項目的key>
      ~~客製化欄位~~
    </template>
  </MyTable>
</template>

接下來會分步驟處理,並講解語法。
大家可以用 fork 這份 code,跟著下面的解說一起練習。

1. named slot

v-slot 可以縮寫成 #,後面接的參數為 slot 區塊的名稱(name 屬性),如果不給予名稱則預設為 default,但 default 只能有一個。

  • 在父層傳入的 <template>,透過 v-slot:slot名稱#slot名稱" 宣告 slot 的名稱
  • 在元件層的 <slot> 區塊,給予 name 屬性,表示要插入的 slot 的名稱

透過對 slot 區塊命名,指定傳入的內容要應用在哪裡。

在子元件內使用 v-for 迭代 tableBody 資料時,當資料(物件)的 key 等於 slot 名稱時,將 slot 內容嵌到 <td> 下。

<tbody>
  <tr v-for="(item, itemIndex) in props.tableBody" :key="item.name">
    <th>{{ itemIndex + 1 }}</th>
    <td v-for="(value, key, index) in item" :key="value">
      <span v-if="如果沒有傳入和 key 相同名稱的 slot">{{ value }}</span>
      <slot v-else :name="key"></slot> <!--重點是這一行!-->
    </td>
  </tr>
</tbody>

2. 取得父層傳入哪些 slot

承上,要如何判別父層傳入哪些 slot?

v-if="如果沒有傳入和 key 相同名稱的 slot"
  • 在 SFC script setup 內,可以使用 useSlots
<script setup>
import { useSlots } from "vue";
const slots = useSlots();
</script>
  • 在 SFC template 內,可以使用 $slot
    $slots 是物件,key 為每個 slot 的名稱,我們可以透過 Object.keys() 將傳入的 slot 名稱迭代出來做判斷。
<tbody>
  <tr v-for="(item, itemIndex) in props.tableBody" :key="item.name">
    <th>{{ itemIndex + 1 }}</th>
    <td v-for="(value, key, index) in item" :key="value">
      <span v-if="!Object.keys($slots).includes(key)">{{ value }}</span>
      <slot v-else :name="key"></slot>
    </td>
  </tr>
</tbody>

4. 傳資料給 slot

因為 slot 內容是在父層定義的,所以可以直接拿到父層的資料沒有辦法拿到元件層的資料,元件層的資料可以透過 props 傳入 <slot> 內。

以剛剛的範例來說:

tableHeadstableBody 是從父層傳入元件的 props,這兩筆資料我們可以直接在元件層拿到;但我們在渲染表格的時候,需要知道透過 v-for 進行列表渲染時,該項 valueindex,才能取到對應的 ithelpLink 連結,而這些渲染資訊(valueindex)被放在子元件內。

<template>
  <MyTable :tableHeads="tableHeads" :tableBody="tableBody">
    <!-- 查看連結 -->
    <template #ithelpLink>
      <a :href="tableBody[index].ithelpLink" target="_blank">查看</a>
    </template>
  </MyTable>
</template>

所以要將子元件的資料傳入元件層的 <slot>

  • name 屬性是用來對應 slot 名稱
  • 可以傳入valueindex、甚至是元件收到的全部 props
<tbody>
  <tr v-for="(item, itemIndex) in props.tableBody" :key="item.name">
    <th>{{ itemIndex + 1 }}</th>
    <td v-for="(value, key, index) in item" :key="value">
      <span v-if="!Object.keys($slots).includes(key)">{{ value }}</span>
      <slot v-else :name="key" :value="value" :index="itemIndex" :props="props"></slot>
    </td>
  </tr>
</tbody>

透過 v-bind 傳入的值,可以在父層從 v-slot 指令拿到。
所有傳給 <slot> 的屬性會被集合成物件可以直接解構需要的屬性,讓模板變得更簡潔。

<MyTable :tableHeads="tableHeads" :tableBody="tableBody">
    <!-- 查看連結 -->
    <template #ithelpLink="{ value, index, props }">
      <!-- 取得當筆 value -->
      <a :href="value" target="_blank">查看</a>
      <!-- 從父層拿 tableBody -->
      <a :href="tableBody[index].ithelpLink" target="_blank">查看</a>
      <!-- 從元件層的 props 拿 tableBody -->
      <a :href="props.tableBody[index].ithelpLink" target="_blank">查看</a>
    </template>
</MyTable>

以今天的範例來說,直接從 value 就可以取得文章連結,主要是為了示範 slot 可以拿到的資料,所以多傳其他屬性,多寫幾種取法。

畫重點:slot 的 scope 在父層,但透過 v-bind,可以同時拿到父層跟子層的資料做運用

最後成果可以到這裡看。
透過運用 v-slot,就可以在使用表格元件時,自訂需要的欄位格式或樣式,要幾個加幾個、想長怎麼樣就長怎麼樣。

這也是為什麼 v-slot 可以讓元件更彈性,在不同的情況下依然能複用。


說真的,想看元件「彈性的極致」,那不就是 UI framework 嗎?他們的 table 元件都有預留 slot 欄位,提供開發者客製化欄位結構或樣式,以 Quasar 為例,他提供的 QTable 就有設計高達 19 個 slot 區塊。

以 BootstrapVue 的 Pagination 為例,在「第一頁」、「前一頁」、「分頁頁數」、「中間刪節」等處都留了 <slot>,讓開發者可以傳進自訂內容和樣式。

當然在一般網站開發下,沒有必要在自訂元件上,預設所有可能用到的 slot 區塊(過度設計),但如果專案有搭配 UI framework 進行開發,多少會需要將自己客製的 template 片段,傳進 UI 框架幫你挖好的 slot 中,所以還是有必要了解 v-slot 的使用方式!

Renderless component - Reusable Transitions

Vue 在 slot guide 篇章提了一個很有趣的使用概念 - Renderless Components。
我們很常在元件內封裝邏輯和畫面,利用 v-slot,可以將邏輯封裝在元件層,渲染的部份則一樣由父層負責,元件本身不需要負責渲染的工作。

官方文件的案例是一個 MouseTracker,可以在 Vue SFC Playground 玩玩看。

今天會用 Vue 的內建 <Transition> 元件作為說明案例。

<Transition>

先看 <Transition> 的使用方式與效果:
<template>

<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>

<style>

.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

<Transition> 元件本身就有運用到 <slot> 的概念,他會將進入、離開的動畫應用 default slot 的內容上,也就是我們傳入 <Transition></Transition> 內的元素或元件上。

Reusable Transitions

我們可以利用 <Transition> 加上自訂的動畫效果,做成可以複用的 Reusable Transitions,(這其實也是 Vue 在 <Transition> 章節提到的概念)。

SlideFadeTransition.vue

  • <Transition> 一個 name 屬性,要對應到 CSS style 的前綴。
  • 在 template 的部份留下 default slot 即可,讓插入的元素或元件可以插入 <Transition> 內。
<template>
  <Transition name="slide-fade">
    <slot />
  </Transition>
</template>

注意樣式 <style> 不可以加上 scoped 屬性,否則無法應用在父層的 <slot> 上。

.slide-fade-enter-active {
  transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
  transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}

更多可以調用的 CSS class name 可以 參考這邊


App.vue 引用:

<SlideFadeTransition
    <p v-if="show">hello</p>
</SlideFadeTransition>

每次要用到 Slide-Fade Transition 的效果時,就不需要重寫 CSS 樣式,直接引用 <SlideFadeTransition> 即可。

結尾

我覺得 slot 真的要透過實作來練習,像共用表格元件就是一個不錯的練習!(不過實作類型的文章真的好難寫喔QQ)
自訂複用的 transition 元件在 <slot> 的應用上很單純,比較複雜的是在於了解有哪些屬性可以傳進 <Transition> 中,調整動畫的呈現,有興趣的人可以到這裡看文件。

參考資料

  • Vue Doc
  • Quasar
  • BootstrapVue

上一篇
Day 17: 元件溝通的原則 feat. props & emit
下一篇
Day 19: 你可能不知道的 v-model - 為何多選綁定陣列不能用 reactive()?
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

【**此則訊息已被站方移除**】
【**此則訊息已被站方移除**】
0
南國安迪
iT邦新手 3 級 ‧ 2022-10-04 09:26:50

學會了,謝謝

安揪拉 iT邦新手 4 級 ‧ 2022-10-04 17:00:56 檢舉

南國ㄟ安迪 好感人 π__π

我要留言

立即登入留言